package coreModel;



import hitExceptions.InvalidDataException;
import hitExceptions.InvalidOperationException;


import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import common.Result;

import dataPersistence.dao.IItemDAO;
import dataPersistence.dto.ItemDTO;

public class ItemManager extends SerializableImplementer{

	private Map<Product,Collection<Item>> productToItems;
	private Map<ProductContainer, Collection<Item>> containerToItems;
	private Map<ProductContainer, Map<Product,Collection<Item>> >containerToProductsToItems;
	private Map<Barcode, Item> barcodeToItem;

	public ItemManager()
	{
		productToItems = new HashMap<Product,Collection<Item>>();
		containerToItems = new HashMap<ProductContainer,Collection<Item>>();
		containerToProductsToItems = new HashMap<ProductContainer, Map<Product,Collection<Item>>>();
		barcodeToItem = new HashMap<Barcode,Item>();
	}

	
	public boolean addItem(Item i) throws InvalidOperationException
	{		
		return	addItem(i,true);
	}
	/**
	 * Adds an Item into the ItemManager. If Item to be added is a member of a new Product
	 * then that new Product is added to the ProductManager
	 * @param i is the already initialized Item to be added.
	 * @return
	 * @throws InvalidOperationException 
	 */
	public boolean addItem(Item i, boolean addToDB) throws InvalidOperationException
	{		

		if (barcodeToItem==null || i==null || barcodeToItem.containsKey(i.getBarcode()) )
			return false;

		// update the Barcode ==> Item index
		barcodeToItem.put(i.getBarcode(), i);

		IItemDAO dao = HIT.getInstance().getFactory().getItemDAO();
		if(addToDB)
		{
			ItemDTO dto = new ItemDTO(i);
			if(!HIT.getInstance().isLoadingFromDataBase())
			{
				Result result = new Result();
				result =dao.add(dto);
				if(!result.getStatus())
				{
					System.err.println(result.getMessage());
					return false;
				}

			}
		}


		// update the Product ==> Items index
		Product p = i.getProduct();
		ProductManager pMgr = HIT.getInstance().getProductManager();

		boolean hasProduct = pMgr.containsProductInContainer(p, i.getParentContainer());		

		if (!hasProduct)
			pMgr.addProductToContainer(p, i.getParentContainer());

		Collection<Item> pItems;
		if (productToItems.containsKey(p))
			pItems = productToItems.get(p);
		else
			pItems = new HashSet<Item>();

		pItems.add(i);
		productToItems.put(p, pItems);


		// update the ProductContaienr ==> Items index
		ProductContainer pc = i.getParentContainer();
		Collection<Item> pcItems;
		if (containerToItems.containsKey(pc))
			pcItems = containerToItems.get(pc);
		else
			pcItems = new HashSet<Item>();

		pcItems.add(i);
		containerToItems.put(pc, pcItems);


		// update the ProductContainer ==> Products ==> Items index
		Collection <Item> pcProductItems;
		if (containerToProductsToItems.containsKey(pc))
		{
			if ( containerToProductsToItems.get(pc).containsKey(i.getProduct()) )
			{
				containerToProductsToItems.get(pc).get(i.getProduct()).add(i);
			}
			else
			{
				pcProductItems = new HashSet<Item>();
				pcProductItems.add(i);
				containerToProductsToItems.get( pc ).put(i.getProduct(), pcProductItems);
			}
		}
		else
		{
			pcProductItems = new HashSet<Item>();
			pcProductItems.add(i);

			Map<Product, Collection<Item>> pcProducts = new HashMap<Product, Collection<Item>>();
			pcProducts.put(i.getProduct(), pcProductItems);

			containerToProductsToItems.put(pc, pcProducts);
		}

		return true;
	}

	/**
	 * Retrieve an Item by its Barcode
	 * @param b the Barcode of the Item to be retrieved.
	 * @return
	 */
	public Item getItem(Barcode b)
	{
		return barcodeToItem.get(b);
	}

	/**
	 * Remove an Item from the ItemManager
	 * @param b the Barcode of the Item to be removed from the ItemManager
	 * @param updateHistory is set to true if we want to record the removed Item in the 
	 * removedItemsManager. This is useful for transferring Items from one place to another.
	 * @return the Item that was removed. It the Item did not exist, return null
	 */
	public Item removeItem(Barcode b, boolean updateHistory, boolean updateDatabase)
	{
		// Remove from Barcode ==> Item index
		Item i = barcodeToItem.remove(b);

		if (i == null)
			return null;

		try
		{
			if (updateHistory)
				i.setExitTime(new Date());
		} catch (InvalidDataException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// Remove from Product ==> Items index
		Product p = i.getProduct();
		productToItems.get(p).remove(i);

		// Remove from ProductContainer ==> Items index
		ProductContainer pc = i.getParentContainer();
		containerToItems.get(pc).remove(i);

		// Remove from ProductContainer ==> Products ==> Items index
		containerToProductsToItems.get(i.getParentContainer()).get(i.getProduct()).remove(i);

		if (updateHistory)
			HIT.getInstance().getItemHistory().addItem(i, updateDatabase);

		return i;
	}

	/**
	 * Get a list of ALL Items by Product
	 * @param p the Product for which we want to retrieve ALL of its Items
	 * @return a Collection<Item> with ALL of the Items of a given Product
	 */
	public Collection<Item> getAllItemsOfProduct(Product p) 
	{
		if ( !productToItems.containsKey(p) )
			return new ArrayList<Item>(); //changed this because it just makes sense 
		//(And I needed it changed for my ProdStatsReport testing)

		Collection<Item> pItems = productToItems.get(p);

		return pItems;
	}

	/**
	 * Get a list of ALL Items by ProductContainer
	 * @param pc the ProductContainer for which we want to retrieve ALL of its Items
	 * @return a Collection<Item> with ALL of the Items of a given ProductContainer
	 */
	public Collection<Item> getAllItemsOfContainer(ProductContainer pc) 
	{
		if ( !containerToItems.containsKey(pc) )
			return new ArrayList<Item>();
		//changed to fix so it doesn't return null

		Collection<Item> pcItems = containerToItems.get(pc);

		return pcItems;
	}

	/**
	 * Get all of the Items in the ItemManager
	 * @return a Collection<Item> with all the Items in the the ItemManager
	 */
	public Collection<Item> getAllItems()
	{
		return barcodeToItem.values();
	}

	/**
	 * This method is to be used when we need to display all the Items of a given Product
	 * that is found within a given ProductContainer
	 * @param pc the ProductContainer which houses the Product
	 * @param p the Product for which we want to retrieve the Items found in the ProductContainer
	 * @return a Collection<Item> with all the Items of the ProductContainer and Product query
	 */
	public Collection<Item> getItemsByContainerAndProduct(ProductContainer pc, Product p)
	{
		if ( !containerToProductsToItems.containsKey(pc) )
			return null;

		if ( !containerToProductsToItems.get(pc).containsKey(p) )
			return null;

		return containerToProductsToItems.get(pc).get(p);
	}

	/**
	 * Move the Items of a Product in a source ProductContainer to a destination ProductContainer
	 * @param p the Product whose Items we want to retrieve
	 * @param pcSrc the source ProductContainer where the Product is found
	 * @param pcDest the destination ProductContainer where we want to move the Product
	 * @return false if any of the input parameters do not exist. This means the move did not
	 * take place.
	 * @throws InvalidOperationException 
	 */
	public boolean moveItemsByProduct(Product p, ProductContainer pcSrc,
			ProductContainer pcDest) throws InvalidOperationException
			{
		
		if ( !containerToProductsToItems.containsKey(pcSrc) || 
				!containerToProductsToItems.containsKey(pcDest) )
			return false;

		if ( !containerToProductsToItems.get(pcSrc).containsKey(p) )
			return false;
		

		Collection<Item> itemsToMove = 
			new HashSet<Item>(containerToProductsToItems.get(pcSrc).get(p));

		Iterator itr = itemsToMove.iterator();

		IItemDAO dao = HIT.getInstance().getFactory().getItemDAO();
		Result result = new Result();
		while (itr.hasNext())
		{
			
			Item tmpI = (Item)itr.next();

			this.removeItem(tmpI.getBarcode(), false, false);

			tmpI.setParentContainer(pcDest);


			this.addItem(tmpI,false);
			ItemDTO dto = new ItemDTO(tmpI);
			result =dao.update(dto);
			if(!result.getStatus())
			{
				System.err.println( "This is the message "+result.getMessage());
			}
			

		}


		return true;
			}

	public void addContainer(ProductContainer container)
	{
		if(!containerToItems.containsKey(container))
			containerToItems.put(container, new ArrayList<Item>());

		if(!containerToProductsToItems.containsKey(container))
			containerToProductsToItems.put(container, new HashMap<Product,Collection<Item>>());
	}

	/**
	 * Return a Collection<Item> of the expired Items in the ItemManager
	 * @return a Collection<Item> of the expired Items in the ItemManager
	 */
	public Collection<Item> getExpiredItems()
	{
		Collection<Item> expItems = new HashSet();

		Collection<Item> checkMe = barcodeToItem.values();

		Iterator itr = checkMe.iterator();

		while (itr.hasNext())
		{
			Item tmpI = (Item)itr.next();

			if (tmpI.isExpired())
				expItems.add(tmpI);
		}

		return expItems;
	}


	/*
	 * **********************************************************************************
	 * These methods are for testing the functionality of the class
	 * **********************************************************************************
	 * 
	 */
	public static boolean testAddGetRem()
	{
		boolean result = true;

		ItemManager mgr = new ItemManager();
		GregorianCalendar cal = new GregorianCalendar();


		Barcode bc1 = new Barcode();
		Barcode bc2 = new Barcode();
		Barcode bc3 = new Barcode();
		Barcode bc4 = new Barcode();

		Item item1 = new Item();
		item1.setBarcode(bc1);
		try{ item1.setEntryDate(cal.getTime()); }
		catch (Exception e) { }

		cal.set(2012, 0, 5);
		Item item2 = new Item();
		item2.setBarcode(bc2);
		try{ item2.setEntryDate(cal.getTime()); }
		catch (Exception e) { }

		cal.set(2012, 0, 15);
		Item item3 = new Item();
		item3.setBarcode(bc3);
		try{ item3.setEntryDate(cal.getTime()); }
		catch (Exception e) { }

		cal.set(2012, 0, 20);
		Item item4 = new Item();
		item4.setBarcode(bc2);
		try{ item4.setEntryDate(cal.getTime()); }
		catch (Exception e) { }


		//		result = mgr.addItem(item1);
		//		if (result)
		//			System.out.println("Itm 1 added.");
		//		else
		//			System.out.println("Itm 1 not added!");
		//		
		//		result = mgr.addItem(item2);
		//		if (result)
		//			System.out.println("Itm 2 added.");
		//		else
		//			System.out.println("Itm 2 not added!");
		//		
		//		result = mgr.addItem(item3);
		//		if (result)
		//			System.out.println("Itm 3 added.");
		//		else
		//			System.out.println("Itm 3 not added!");
		//		
		//		result = mgr.addItem(item4);
		//		if (result)
		//			System.out.println("Itm 4 added.");
		//		else
		//			System.out.println("Itm 4 not added!");

		System.out.println("\n" + mgr.getAllItems() + "\n");

		System.out.println(mgr.getItem(bc1).getEntryDate());
		System.out.println(mgr.getItem(bc2).getEntryDate());
		System.out.println(mgr.getItem(bc3).getEntryDate());

		System.out.println("Rem: " + mgr.removeItem(bc3, false, false).getEntryDate());
		System.out.println(mgr.removeItem(bc4, false, false));

		System.out.println("\n" + mgr.getAllItems() + "\n");
		return result;
	}

	/**
	 * Removes a container from the indexes of items
	 * @param storageUnit the storageUnit to be removed
	 * @throws InvalidOperationException if container is non empty
	 */
	public void removeContainer(ProductContainer storageUnit) throws InvalidOperationException
	{
		//remove from first index
		if(containerToItems.containsKey(storageUnit))
		{
			if(!containerToItems.get(storageUnit).isEmpty())
			{
				throw new InvalidOperationException("Cannot remove a ProductContainer " +
				"that has items in it");
			}
			containerToItems.remove(storageUnit);
		}

		//remove from second index
		if(containerToProductsToItems.containsKey(storageUnit))
		{
			//set of product keys in containerToProductToItems map
			Set<Product> productGroupSet = containerToProductsToItems.get(storageUnit).keySet();
			for(Product p: productGroupSet)
			{	//if any of them is non empty we can not remove the parent
				if (!containerToProductsToItems.get(storageUnit).get(p).isEmpty())
				{
					throw new InvalidOperationException("Cannot remove a ProductContainer " +
					"that has items in it");
				}
			}
			containerToProductsToItems.remove(storageUnit);
		}

	}

	public boolean canRemove(Product prod, ProductContainer container)
	{
		// added for a null check because bug 10 Functionality of selecting and editing a 
		//Product from the Root was making this throw a null pointer exception
		if(!containerToProductsToItems.containsKey(container)||
				!containerToProductsToItems.get(container).containsKey(prod))
			return false;

		// if the container doesn't container any of the products
		if(containerToProductsToItems.get(container).get(prod).isEmpty())
			return true;
		return false;
	}

	public boolean canRemoveProductFromAll(Product prod)
	{
		//if there is no items of this product in the whole system
		if (productToItems.containsKey(prod) && 
				productToItems.get(prod).isEmpty())
		{
			return true;
		}
		else
			return false;
	}

	/** FUNCTION ADDED BY TYLER FOR EASE OF USE
	 * Moves an item to the appropriate container. (And updates its parentContainer attribute.)
	 * 
	 * @param item The item to be moved.
	 * @param destContainer The location to move the item.
	 * @throws InvalidOperationException When the container does not exist in the map.
	 */
	public void moveItem(Item item, ProductContainer destContainer) 
	throws InvalidOperationException
	{
		if (!containerToProductsToItems.containsKey(destContainer))
			throw new InvalidOperationException("Cannot move an item to an unindexed "
					+ "(and probably nonexistent) container.");

		this.removeItem(item.getBarcode(), false, false);

		item.setParentContainer(destContainer);

		this.addItem(item,false);
	}

	public void removeProductFromContainer(Product product,
			ProductContainer container) throws InvalidOperationException
			{
		if(!containerToProductsToItems.containsKey(container) || // doesn't contain key
				!containerToProductsToItems.get(container).containsKey(product) || // no prod
				!containerToProductsToItems.get(container).get(product).isEmpty()) // not empty
		{
			throw new InvalidOperationException("Can't remove that product");
		}

		productToItems.remove(product);
		containerToProductsToItems.get(container).remove(product);

			}

	public void removeProductFromProductlist(Product product) throws InvalidOperationException
	{
		if(!productToItems.containsKey(product) ||
				!productToItems.get(product).isEmpty())
		{
			return; // Ammon - I added 'return' here because this line was just a semi-colon.
			//			Seems like it needs to be here.
		}
		productToItems.remove(product);
	}

	public boolean hasContainer(ProductContainer pc)
	{
		if ( containerToItems.containsKey(pc) && containerToProductsToItems.containsKey(pc))
			return true;
		else if ( !containerToItems.containsKey(pc) && !containerToProductsToItems.containsKey(pc))
			return false;
		else
			throw new RuntimeException("ItemManager: maps are out of sync for ProductContainers.");
	}

}
